gh-125400: add async generator return value#125401
gh-125400: add async generator return value#125401alex-dixon wants to merge 3 commits intopython:mainfrom
Conversation
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
|
Thanks for writing a reference implementation for this idea as was requested in the Discourse thread; it makes it much easier to evaluate the proposal! I'm marking the PR as |
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
ZeroIntensity
left a comment
There was a problem hiding this comment.
Some initial comments. I do realize that a lot of this is copied from the StopIteration implementation, so it might be better to ignore some of my comments for consistency (or better yet, apply my comments to StopIteration as well 😄)
Overall, I think this is nice for consistency with StopIteration, but I don't think this is particularly useful without support for yield from in async generators, because you can implement this just fine in current versions--just define an exception with a value attribute, and raise it.
| return -1; | ||
| } | ||
| if (value == NULL) { | ||
| value = Py_NewRef(Py_None); |
There was a problem hiding this comment.
Py_None is immortal, I don't think we need to incref it. There's some contention about this, though.
| value = Py_NewRef(Py_None); | |
| value = Py_None; |
| PyObject *value = NULL; | ||
| if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) { | ||
| PyObject *exc = PyErr_GetRaisedException(); | ||
| value = Py_NewRef(((PyStopAsyncIterationObject *)exc)->value); | ||
| Py_DECREF(exc); | ||
| } else if (PyErr_Occurred()) { | ||
| return -1; | ||
| } |
There was a problem hiding this comment.
There's going to be a lot of internal reusing of _PyThreadState_GET with this approach. You can use the private API:
| PyObject *value = NULL; | |
| if (PyErr_ExceptionMatches(PyExc_StopAsyncIteration)) { | |
| PyObject *exc = PyErr_GetRaisedException(); | |
| value = Py_NewRef(((PyStopAsyncIterationObject *)exc)->value); | |
| Py_DECREF(exc); | |
| } else if (PyErr_Occurred()) { | |
| return -1; | |
| } | |
| PyObject *value = NULL; | |
| PyThreadState *tstate = _PyThreadState_GET(); | |
| PyObject *occurred = _PyErr_Occurred(tstate); | |
| if (PyErr_GivenExceptionMatches(occurred, PyExc_StopAsyncIteration)) { | |
| PyObject *exc = _PyErr_GetRaisedException(tstate); | |
| value = Py_NewRef(((PyStopAsyncIterationObject *)exc)->value); | |
| Py_DECREF(exc); | |
| } else if (occurred) { | |
| return -1; | |
| } |
| if (value == NULL || | ||
| (!PyTuple_Check(value) && !PyExceptionInstance_Check(value))) | ||
| { | ||
| /* Delay exception instantiation if we can */ | ||
| PyErr_SetObject(PyExc_StopAsyncIteration, value); | ||
| return 0; | ||
| } | ||
| /* Construct an exception instance manually with | ||
| * PyObject_CallOneArg and pass it to PyErr_SetObject. | ||
| * | ||
| * We do this to handle a situation when "value" is a tuple, in which | ||
| * case PyErr_SetObject would set the value of StopIteration to | ||
| * the first element of the tuple. | ||
| * | ||
| * (See PyErr_SetObject/_PyErr_CreateException code for details.) | ||
| */ | ||
| e = PyObject_CallOneArg(PyExc_StopAsyncIteration, value); | ||
| if (e == NULL) { | ||
| return -1; | ||
| } | ||
| PyErr_SetObject(PyExc_StopAsyncIteration, e); | ||
| Py_DECREF(e); |
There was a problem hiding this comment.
Same story here--use the thread state.
| if (value == NULL || | |
| (!PyTuple_Check(value) && !PyExceptionInstance_Check(value))) | |
| { | |
| /* Delay exception instantiation if we can */ | |
| PyErr_SetObject(PyExc_StopAsyncIteration, value); | |
| return 0; | |
| } | |
| /* Construct an exception instance manually with | |
| * PyObject_CallOneArg and pass it to PyErr_SetObject. | |
| * | |
| * We do this to handle a situation when "value" is a tuple, in which | |
| * case PyErr_SetObject would set the value of StopIteration to | |
| * the first element of the tuple. | |
| * | |
| * (See PyErr_SetObject/_PyErr_CreateException code for details.) | |
| */ | |
| e = PyObject_CallOneArg(PyExc_StopAsyncIteration, value); | |
| if (e == NULL) { | |
| return -1; | |
| } | |
| PyErr_SetObject(PyExc_StopAsyncIteration, e); | |
| Py_DECREF(e); | |
| PyThreadState *tstate = _PyThreadState_GET(); | |
| if (value == NULL || | |
| (!PyTuple_Check(value) && !PyExceptionInstance_Check(value))) | |
| { | |
| /* Delay exception instantiation if we can */ | |
| _PyErr_SetObject(tstate, PyExc_StopAsyncIteration, value); | |
| return 0; | |
| } | |
| /* Construct an exception instance manually with | |
| * PyObject_CallOneArg and pass it to PyErr_SetObject. | |
| * | |
| * We do this to handle a situation when "value" is a tuple, in which | |
| * case PyErr_SetObject would set the value of StopIteration to | |
| * the first element of the tuple. | |
| * | |
| * (See PyErr_SetObject/_PyErr_CreateException code for details.) | |
| */ | |
| e = PyObject_CallOneArg(PyExc_StopAsyncIteration, value); | |
| if (e == NULL) { | |
| return -1; | |
| } | |
| _PyErr_SetObject(tstate, PyExc_StopAsyncIteration, e); | |
| Py_DECREF(e); |
| Py_ssize_t size = PyTuple_GET_SIZE(args); | ||
| PyObject *value; | ||
|
|
||
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) |
There was a problem hiding this comment.
Typically, the convention is to use < 0 rather than == -1
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) | |
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) < 0) |
| if (size > 0) | ||
| value = PyTuple_GET_ITEM(args, 0); | ||
| else | ||
| value = Py_None; | ||
| self->value = Py_NewRef(value); |
There was a problem hiding this comment.
This can get simplified a little, as None is immortal like I mentioned:
| if (size > 0) | |
| value = PyTuple_GET_ITEM(args, 0); | |
| else | |
| value = Py_None; | |
| self->value = Py_NewRef(value); | |
| if (size > 0) { | |
| self->value = Py_NewRef(PyTuple_GET_ITEM(args, 0)); | |
| } | |
| else { | |
| self->value = Py_None; | |
| } |
|
|
||
| if (BaseException_init((PyBaseExceptionObject *)self, args, kwds) == -1) | ||
| return -1; | ||
| Py_CLEAR(self->value); |
There was a problem hiding this comment.
Why CLEAR it here? It's an extra operation of setting it to NULL when nothing will touch it in between calls here anyway.
| Py_CLEAR(self->value); | |
| Py_DECREF(self->value); |
| if (result == Py_None) { | ||
| PyErr_SetNone(PyExc_StopAsyncIteration); | ||
| } | ||
| else { | ||
| _PyGen_SetStopAsyncIterationValue(result); | ||
| } |
There was a problem hiding this comment.
Consider acquiring the thread state here, and then passing that to _PyGen_SetStopAsyncIterationValue (to omit an extra call there)
| async def gen(): | ||
| yield 1 | ||
| yield 2 | ||
| return 3 |
There was a problem hiding this comment.
It might be a good idea to add a dummy coroutine (something like asyncio.sleep(0) should work) here, as that needs to get yielded in a different way--in my experience with dealing with the coroutine implementation, a coroutine that awaits nothing behaves differently than one that does.
There was a problem hiding this comment.
Thanks so much for your thorough and prompt review. I've updated this to include awaited calls to asyncio.sleep.
|
Most changes to Python require a NEWS entry. Add one using the blurb_it web app or the blurb command-line tool. If this change has little impact on Python users, wait for a maintainer to apply the |
I believe the strongest argument in favor of this feature even without a Separately from the above, IIUC, @MadcowD might have been interested in writing a reference implementation for |
📚 Documentation preview 📚: https://cpython-previews--125401.org.readthedocs.build/